package cluedo;

import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import msgcomm.Receiver;


import cluedo.PlayerState.RawLtsState;

import static cluedo.Player.log;


/**
 * Uno zombie e' un player che, durante lo svolgimento di un partita, viene a 
 * mancare. Questo puo' avvenire sia perche' e' lui a decidere di uscire, sia a 
 * causa di un suo crash. In gergo viene detto che "il player e' diventato uno 
 * zombie". Un giocatore che ha perso pero' non diventa automaticamente uno 
 * zombie, perche' puo' sempre decidere quali carte far vedere ad un giocatore 
 * che ha fatto un'ipotesi.
 * Uno zombie si comporta come un player passivo: non si muove e non fa 
 * ipotesi/accuse. Puo' solo mostrare le sue carte in caso di ipotesi. In caso 
 * abbia piu' carte facenti parte della stessa ipotesi, ne verra' scelta una a 
 * caso. Le azioni che uno zombie-morto deve compiere sono decise ed hanno
 * effetto solo a livello locale. L'unica eccezione si ha quando muore il
 * "primo proprietario" (il proprietario al momento dell'inizio del gioco). In
 * questo caso la distribuzione delle carte (con conseguenti chiamate remote)
 * vengono fatte dallo zombi-morto corrispondente al "primo proprietario" che
 * si trova all'interno dell'attuale proprietario.
 * 
 * Ogni istanza di Zombie e' associata con uno specifico player remoto oppure 
 * al player locale.
 * Inizialmente gli zombi sono vivi e il loro scopo e' mantenere l'informazione
 * delle carte possedute da ciascun giocatore. Quando un player va in crash, 
 * la relativa istanza zombie-vivo diventa zombie-morto.
 * 
 * L'idea aggiuntiva e' che invece di comunicare direttamente con il player remoto,
 * la comunicazione passa per lo zombie ad esso associato. Se lo zombie e vivo,
 * la comunicazione viene inoltrata al player remoto, altrimenti (caso zombie-morto)
 * la comunicazione viene gestita localmente.
 * 
 * NOTA: Durante la creazione di una istanza di zombi (vedi costruttore),
 * non viene creata una copia locale di playGroupMembers, ma viene
 * tenuto un riferimento all'istanza passata come parametro nel costruttore (presubimilmente
 * appartenente al player locale che ha creato lo zombie).
 * Questo permette di non dovere notificare lo zombie con metodi ad-hoc quando
 * altri player diventano zombie. Tuttavia bisogna utilizzare le primitive
 * di sincronizzazione quando si usa questa mappa per evitare conflitti.
 */
public class Zombie implements StateChangeListener {
	// TODO assumo che turnIndex() non restituisca mai -1 perche' richiamato in modo corretto
	
	/**
	 * true se il player associato non e' in crash, false altrimenti
	 */
	private boolean alive;
	/**
	 * Nickname del player remoto associato
	 */
	private String nickname;
	/**
	 * tranceiver del player remoto associato. null se zombie-morto
	 */
	private Receiver linkedPlayerTrans;
	/**
	 * Player locale
	 */
	private Player localPlayer;
	/**
	 * true se lo zombie e' associato al Player locale, false altrimenti
	 */
	private boolean isLocalPlayer;
	/**
	 * Lista delle carte di questo zombie
	 */
	private List<Card> myCards;
	/**
	 * Mappa fra nickname (String) e oggetto remoto (PlayerIntf) dei player.
	 * E' un riferimento alla mappa passata come parametro nel costruttore e non
	 * una copia locale di tale mappa.
	 */
	private final LinkedHashMap<String, Zombie> playGroupMembers;
	/**
	 * La pedina associata allo zombie
	 */
	private Pawn pawn;
	/**
	 * controlla se devo partecipare ad uno spareggio successivo al primo
	 */
	private boolean mustDoTieBreak=false;
	
	/**
	 * Stato dello zombie
	 */
	protected PlayerState state;
	
	
	 /* Variabili utili per permettere allo zombie-morto di poter rispondere riguardo un'ipotesi se il player 
		remoto associato fa crash */
	/**
	 * player che ha fatto l'ultima ipotesi
	 */
	private String playerAssumption=null;
	/**
	 * carta personaggio dell'ultima ipotesi ricevuta
	 */
	private Character characterAssumption=null;
	/**
	 * carta arma dell'ultima ipotesi ricevuta
	 */
	private Weapon weaponAssumption=null;
	/**
	 * carta stanza dell'ultima ipotesi ricevuta
	 */
	private Room roomAssumption=null;
	
	
	/**
	 * Crea uno zombie-vivo inizializzandolo allo stato GROUP_MEMBER.
	 * Utilizzare questo costruttore per gli zombie associati a player remoti.
	 * 
	 * NOTA: non viene creata una copia locale di playGroupMembers, ma viene
	 * tenuto un riferimento all'istanza passata come parametro (presubimilmente
	 * appartenente al player locale che ha creato lo zombie).
	 * @param localPlayer player locale che gestisce questo zombie
	 * @param linkedPlayerName player remoto associato
	 * @param linkedPlayerTrans tranceiver del player remoto associato
	 * @param playGroupMembers membri del gruppo
	 * @param owner true se e' il proprietario del gruppo, false altrimenti
	 */
	public Zombie(
			Player localPlayer,
			String linkedPlayerName,
			Receiver linkedPlayerTrans, 
			LinkedHashMap<String, Zombie> playGroupMembers,
			boolean owner) {
		
		this.alive= true;
		this.nickname= linkedPlayerName;
		this.linkedPlayerTrans= linkedPlayerTrans;
		this.localPlayer= localPlayer;
		this.isLocalPlayer= false;
		this.playGroupMembers= playGroupMembers;
		this.state= new PlayerState(nickname, owner);
		state.addStateChangeListener(this);
	}
	
	/**
	 * Crea uno zombie-vivo usando come stato quello passato come parametro.
	 * Utilizzare questo costruttore per lo zombie associato al player locale.
	 * 
	 * NOTA:
	 *  - non viene creata una copia locale di playGroupMembers, ma viene
	 *    tenuto un riferimento all'istanza passata come parametro (presubimilmente
	 *    appartenente al player locale che ha creato lo zombie).
	 *  - non viene creato una nuova istanza di PlayerState, ma viene usato
	 *    il riferimento all'istanza passata come parametro
	 * @param localPlayer player locale associato
	 * @param localPlayerTrans tranceiver del player locale associato
	 * @param playGroupMembers membri del gruppo
	 * @param state stato del player
	 */
	public Zombie(
			Player localPlayer,
			Receiver localPlayerTrans,
			LinkedHashMap<String, Zombie> playGroupMembers,
			PlayerState state) {
		this.alive= true;
		this.nickname= localPlayer.getNickname();
		this.linkedPlayerTrans= localPlayerTrans;
		this.localPlayer= localPlayer;
		this.isLocalPlayer= true;
		this.playGroupMembers= playGroupMembers;
		this.state= state;
		state.addStateChangeListener(this);
	}

	@Override
	protected void finalize() {
		state.removeStateChangeListener(this);
	}
	
	/**
	 * Restituisce true se questo e' lo zombie associato al player locale, false
	 * altrimenti
	 * @return vedi descrizione
	 */
	public boolean isLocalPlayer() {
		return isLocalPlayer;
	}
	
	/**
	 * Restituisce true se questo zombie e' vivo
	 * @return vedi descrizione
	 */
	public boolean isAlive() {
		return alive;
	}

	/**
	 * Trasforma lo zombie in uno zombie-morto.
	 */
	public void markAsDead() {

		if (alive) {
			log.print("Zombie: \"" + nickname + "\" e' stato marcato come morto");
			alive= false;
			linkedPlayerTrans= null;
		}
	}
	
	/**
	 * Se era attesa un'azione dal player remoto (morto), viene eseguita 
	 * l'azione opportuna.
	 */
	public void runDeadZombie() {

		if (isAlive()) return;
		// sono uno zombie-morto
		
		switch (state.getRawLtsState()) {
		
		case CHOOSING_MY_PAWN:
			// seleziono la pedina
			chooseAPawn();
			break;
			
		case TIE_BREAK:
			// tiro il dado
			doTieBreak();
			break;

		case MY_TURN:
			// passo il turno
			skipTurn();
			break;
			
		case WAITING_REFUTE:
			abortAssumption();
			break;
			
		case REFUTE_ASSUMPTION:
			//comunico se posso confutare l'ipotesi oppure no
			synchronized (localPlayer) {
				examineAssumption(playerAssumption,characterAssumption,weaponAssumption,roomAssumption);
			}
			break;
			
		default:
			// niente da fare qui
			break;		
		}
	}
	
	/**
	 * Questo metodo viene invocato quando cambia lo stato dello zombie. I 
	 * cambiamenti relativi a zombie-vivi vengono ignorati e sono presi in
	 * considerazione solo quelli degli zombie-morti
	 * @param newState il nuovo stato
	 */
	@Override
	public void stateChange(PlayerState previousState) {
		// eseguo eventualmente delle azioni solo se questo e' uno zombie-morto
		if (isAlive()) {
			// zombie-vivo: lascio fare le azione al player remoto associato
			return;
		}
		
		
		// impedisco che lo zombie-morto inizi a giocare in modo automatico
		   if (!previousState.isPlaying() && state.isPlaying()) return;
		   
		
		if (
			previousState.getRawLtsState().equals(state.getRawLtsState()) &&
			previousState.isGroupOwner() != state.isGroupOwner() ) {
			// lo stato e' rimasto lo stesso, e' solo cambiato di proprietario
			// Non faccio niente se e' solo cambiato il proprietario
			return;
		}
		
		/* Lo zombie esegue un compito alla volta
		 * Utile, ad esempio, nel caso in cui un player che ha il turno ed proprietario
		 * esce diventando uno zombie morto. Infatti verrebbe
		 * invocato 2 volte stateChange() creando 2 threads che si sovrappongono
		 * generando risultati imprevisti.
		 */
		
		runDeadZombie();
	}
	
	
	/**
	 * Usato per far si che questo zombie partecipi o meno ad uno spareggio successivo al primo
	 * @param b true se lo zombie partecipa al prossimo spareggio, false altrimenti
	 */
	public void mustDoTieBreak(boolean b){
		
		mustDoTieBreak=b;
		
	}
	
	/**
	 * Determina se bisogna fare uno spareggio per determinare il primo giocatore.
	 * Restituisce il numero di player coinvolti nello spareggio, 0 se non e'
	 * necessario alcun spareggio
	 * @return vedi descrizione
	 */
	public int tieBreak() {
		ColorPawn missRed= ColorPawn.RED;
		LinkedList<ColorPawn> membersPawn= new LinkedList<ColorPawn>();
		
		synchronized (localPlayer) {
			for (Map.Entry<String, Zombie> entry : playGroupMembers.entrySet()) {
					membersPawn.add(entry.getValue().getPawnColor());
			}
		}
		
		if (membersPawn.contains(missRed)) {
			// qualcuno ha Miss Scarlett: nessuno spareggio
			return 0;
		} else {
			// si deve spareggiare
			int n= playGroupMembers.size();
			if (n==3) return 3;
			else if (n==4) return 2;
			else if (n==5) return 2;
			else if (n==6) return 0;
			else {
				System.err.println("Errore nel numero di componenti del gruppo");
				return 0;
			}
		}
	}
	
	/**
	 * Restituisce true se questo zombie deve partecipare allo spareggio per
	 * il primo giocatore, false altrimenti
	 * @param first true se e' il primo spareggio, false altrimenti
	 * @return vedi descrizione
	 */
	public boolean joinTieBreak(boolean first) {
		
		if(mustDoTieBreak && !first) { // e' uno spareggio successivo al primo e devo partecipare
			return true;
		}
		else if(!mustDoTieBreak && !first) return false; // e' uno spareggio successivo al primo e non devo partecipare
		
		//else e' il primo spareggio
		
		if (tieBreak()==0) return false; // sta giocando Miss Scarlett
		
		boolean joinTieBreak= false;
		int nPlayers= playGroupMembers.size();
		int nCards= myCards.size();
		
		// se il numero di giocatori e' = 3 tutti devono partecipare allo spareggio se nessuno ha scelto Miss Scarlett
		if (nPlayers==3) joinTieBreak= true;
		// altrimenti se il numero di giocatoi e' 4 o 5  lo spareggio avverra' solo tra i partecipanti ai quali e' toccata una carta in meno.
		else if ( (nPlayers==4) && (nCards==4) ) joinTieBreak= true;
		else if ( (nPlayers==5) && (nCards==3) ) joinTieBreak= true;
		
		return joinTieBreak;
	}
	
	/**
	 * lo zombie e'nello stato WAITING_REFUTE
	 * muore il player mentre aspettava la confutazione delle ipotesi
	 */
	private void abortAssumption() {
		// simulazione confutazione da parte di tutti i player/zombie
		for (Map.Entry<String, Zombie> entry : playGroupMembers.entrySet()) {
			Zombie z= entry.getValue();
			if (z.state.iAmRefutingAssumption()) {
				z.state.answerRefute();
			}
		}
		
		state.getAnswers(); // torno nello stato MY_TURN
	}
	
	/**
	 * Simula la scelta della pedina da parte dello zombie-morto.
	 * Viene presa la lista di tutti i colori disponibili cosi' come definita da
	 * <code>ColorPawn.allColors</code> e vengono rimossi i colori scelti da altri
	 * player. Lo zombie sceglie il primo colore rimasto nella lista.
	 * Notifica di cio' il player locale e gli altri zombie-morti locali.
	 */
	private void chooseAPawn() {
		if (isAlive()) return;
		// sono uno zombie-morto
		
		LinkedList<ColorPawn> pawns= new LinkedList<ColorPawn>();
		
		log.print("Zombie: Il fantasma dello zombie-morto \"" + nickname + "\" sta per scegliere la sua pedina.");

		
		// ottengo la lista di tutti i colori
		for (ColorPawn c : ColorPawn.allColors) {
			pawns.add(c);
		}

		synchronized (localPlayer) {
			// rimuovo i colori gia' scelti
			for (Map.Entry<String, Zombie> entry : playGroupMembers.entrySet()) {
					Zombie z= entry.getValue();
					if (z.state.getRawLtsState() == RawLtsState.WAITING_AFTER_PAWN) {
						// lui ha gia' scelto la pedina
						ColorPawn c= z.getPawnColor();
						pawns.remove(c);
					} else {
						/* 
						 * Le pedina vengono scelte in ordine: mi fermo dopo aver
						 * trovato il primo a non averla ancora scelta (questo mi
						 * dice che anche tutti i player non l'hanno scelta)
						 */
						break;
					}
			}
			
			// prendo il primo colore della lista rimasto
			pawn= new Pawn(pawns.getFirst());

			// notifico al solo player locale (e' lui che poi aggiorna gli zombie di playGroupMembers)
			//localPlayer.multicastNotifyPlayerPawn(nickname, localPlayer.getSequenceNumber(nickname), pawn);
			localPlayer.notifyPlayerPawn(nickname, pawn);
		}
	}
	
	/**
	 * Simula il tiro di dado per lo spareggio da parte dello zombie-morto.
	 * Il risultato del dado e' sempre 1, in modo da non essere mai il primo giocatore.
	 * Notifica di cio' il player locale e gli altri zombie-morti locali.
	 */
	private void doTieBreak() {
		if (isAlive()) return;
		// sono uno zombie-morto
		
		log.print("Il fantasma dello zombie-morto \"" + nickname + "\" sta per tirare per lo spareggio (tira sempre 1).");

		// notifico al solo player locale (e' lui che poi aggiorna gli zombie di playGroupMembers)
		localPlayer.throwTieBreakDice(nickname, 1);
	}
	
	/**
	 * Simula il turno dello zombie-morto.
	 * Lo zombie-morto non fa niente e passa semplicemente il turno
	 * Notifica di cio' il player locale e gli altri zombie-morti locali.
	 */
	private void skipTurn() {
		if (isAlive()) return;
		// sono uno zombie-morto
				
		log.print("Zombie: Il fantasma dello zombie-morto \"" + nickname + "\" sta per saltare il turno.");
		
		//Si puo' verificare un problema dovuto al seguente scenario:
		/* Un player termina il turno e fa multicastEndTurn per comunicarlo a tutti gli altri player remoti
		 * Il player a cui deve essere passato il turno pero' e' uscito;
		 * Quindi e' lo zombie associato che risponde facendo skipTurn();
		 * Ma lo zombie siccome e' locale al player che ha passato il turno puo' rispondere prima che
		 * sia terminato il multicastEndTurn portando ad un ciclo in cui 
		 * Usando il synchronized(localPlayer) ed un altro thread ci assicuriamo che lo zombie non faccia skipTurn prima che 
		 * sia terminato il multicastEndTurn (che ha gia' il lock su playGroupMembers).
		 * Viene usato un altro thread (Ghost) perche' altrimenti il codice sarebbe eseguito ugualmente perche' e' lo
		 * stesso thread che fa multicastEndTurn (che ha gia' il permesso di accedere a playGroupMember) a fare anche SkipTurn.
		 */
				
		
		// notifico al solo player locale (e' lui che poi aggiorna gli zombie di playGroupMembers)
		localPlayer.endTurn(nickname);
			
	}	
	
	
	/**
	 * Lo zombie-morto comunica al player che ha effettuato 
	 * un'ipotesi se puo' confutarla oppure no
	 * @param playerName il nome del player che ha fatto l'ipotesi
	 * @param character la carta personaggio dell'ipotesi
	 * @param weaponl a carta arma dell'ipotesi
	 * @param room la carta stanza dell'ipotesi
	 */
	public void examineAssumption(String playerName, Character character, Weapon weapon, Room room){
		//lo zombie-morto confuta l'ipotesi
		
		log.print("Zombie: Il fantasma dello zombie-morto \"" + nickname + "\" sta per esaminare l'ipotesi.");

		
		if(myCards.indexOf(character)!=-1 || myCards.indexOf(weapon)!=-1 || myCards.indexOf(room)!=-1){
			
			//posso confutare l'ipotesi, invio sempre la prima carta che puo' confutarla
				
				String name=null;
			
				for(Card c: myCards){
					
					name=c.getName();
					
					if(name.equals(character.getName()) ||
							name.equals(weapon.getName())|| 
							name.equals(room.getName())){
						
						if(c instanceof Weapon)									
							localPlayer.refuteAssumption(nickname,new Weapon(name));
						else if(c instanceof Character)
							localPlayer.refuteAssumption(nickname,new Character(name));
						else if(c instanceof Room)
							localPlayer.refuteAssumption(nickname,new Room(name));
						
						
						break;
						
					}
					
				}
		
			
		} 
		else{ //non posso confutare l'ipotesi
	
			localPlayer.refuteAssumption(nickname, null);

		}
		
	}
	
	
	/**
	 * Restituisce il transceiver del player associato a questo zombie.
	 * @return vedi descrizione
	 */
	public Receiver getLinkedPlayerTrans() {
		return linkedPlayerTrans;
	}

	
	/**
	 * Restituisce la posizione di questo player all'interno del gruppo di gioco (usando 0 come prima
	 * posizione). Questa posizione non coincide con l'indice del turno di gioco
	 * (ad es. un valore restituito pari a 0 non significa che e' il primo a
	 * giocare). Si tratta invece della posizione in cui questo player si e' unito
	 * al gruppo.
	 * In altre parole l'indice che occupa il player all'interno di playGroupsMembers
	 * @return la posizione del playernel gruppo di gioco, -1 se il nickname non e' presente in playGroupsMembers
	 */
	private int memberIndex() {
		return memberIndex(getNickname());
	}
 
	/**
	 * Restituisce la posizione del player passato come parametro all'interno del gruppo di gioco (usando 0 come prima
	 * posizione). Questa posizione non coincide con l'indice del turno di gioco
	 * (ad es. un valore restituito pari a 0 non significa che e' il primo a
	 * giocare). Si tratta invece della posizione in cui questo player si e' unito
	 * al gruppo.
	 * In altre parole l'indice che occupa il player all'interno di playGroupsMembers
	 * @return l'indice del turno del giocatore, -1 se il nickname non e' presente in playGroupsMembers
	 */
	private int memberIndex(String name) {
		int ret= 0;
		boolean found= false;
		
		synchronized (localPlayer) {
			for (Map.Entry<String, Zombie> entry : playGroupMembers.entrySet()) {
				if (entry.getKey().equals(name)) {
					// trovato
					found= true;
					break;
				} else {
					ret++;
				}
			}
		}
		
		if (found) return ret;
		else return -1;
	}
	
	/**
	 * Imposta la pedina di questo zombie
	 * @param pawn la pedina di questo zombie
	 */
	
	public void setZombiePawn(Pawn pawn) {
		this.pawn= pawn;
	}
	
	public void setZombieDeck(Room room, Weapon weapon, Character character, List<Card> deck) {
		// delle carte della soluzione non me ne faccio niente... le scarto
		
		int n= playGroupMembers.size();
    	
    	// prelevo le mie carte
    	myCards= new LinkedList<Card>();
    	int j= memberIndex();
    	int nCards= deck.size();
    	while (j<nCards) {
    		myCards.add(deck.get(j));
    		
    		j= j+n;
    	}
    	
    	log.printLog("Zombie \"" + getNickname() + "\": " + myCards);
    	
	}
	
	/**
	 * Memorizza chi ha fatto l'ipotesi e con che carte per potarla confutare con uno zombie morto
	 */
	public void setAssumption(String sender, Character character, Weapon weapon, Room room){
		
		this.playerAssumption=sender;
		this.characterAssumption=character;
		this.weaponAssumption=weapon;
		this.roomAssumption=room;
	}
	
	public String getNickname() {
		return nickname;
	}


	public ColorPawn getPawnColor() {
		
		return pawn.getColor();
	}

	/**
	 * Restituisce la pedina associata allo zombie
	 * @return vedi descrizione
	 */
	public Pawn getPawn(){
		
		return pawn;
	}
	
	// ALTRO
	
	@Override
	public String toString() {
		String ad;
		if (alive) ad="alive";
		else ad="dead";
		
		return "\"" + nickname + "\" is " + ad;
	}

	
	

}


